#include "Camera.h"

#include "Core/GlobalContext.h"

namespace SE
{
	Camera::Camera()
		: mPosition(Vector3f(0.0f)),
		  mOrientation(Vector3f(0.0f)),
		  mOrthographicSize(10.0f),
		  mOrthographicNear(-1.0f),
		  mOrthographicFar(1.0f),
		  mPerspectiveFOV(45.0f),
		  mPerspectiveNear(0.1f),
		  mPerspectiveFar(1000.0f)
	{
		recalculateOrthographicMatrices();
		recalculatePerspectiveMatrices();
	}

	Camera::~Camera()
	{
	}

	void Camera::rotate(const Vector3f& axis, const Float angle)
	{
		SE_PROFILE_FUNCTION();

		mOrientation = glm::normalize(glm::angleAxis(angle, axis) * mOrientation);
		recalculateOrthographicView();
		recalculatePerspectiveView();
	}

	void Camera::rotatePitch(const Float angle)
	{
		SE_PROFILE_FUNCTION();

		mOrientation = glm::normalize(glm::angleAxis(glm::radians(angle), -CameraWorldRight) * mOrientation);
		recalculatePerspectiveView();
	}

	void Camera::rotateYaw(const Float angle)
	{
		SE_PROFILE_FUNCTION();

		mOrientation = glm::normalize(glm::angleAxis(glm::radians(angle), -CameraWorldUp) * mOrientation);
		recalculatePerspectiveView();
	}

	void Camera::rotateRoll(const Float angle)
	{
		SE_PROFILE_FUNCTION();

		mOrientation = glm::normalize(glm::angleAxis(glm::radians(angle), -CameraWorldForward) * mOrientation);
		recalculatePerspectiveView();
	}

	void Camera::onMousePan(const Vector2f& delta)
	{
		SE_PROFILE_FUNCTION();

		mOrientation = glm::normalize(
			glm::angleAxis(glm::radians(delta.y), -CameraWorldRight) * // Pitch.
			glm::angleAxis(glm::radians(delta.x), -CameraWorldUp) * // Yaw.
			mOrientation);
		recalculatePerspectiveView();
	}

	void Camera::onCameraMove(const Vector3f& delta)
	{
		mPosition += delta;
		recalculateOrthographicView();
		recalculatePerspectiveView();
	}

	const Vector3f& Camera::getPosition() const
	{
		return mPosition;
	}

	Float Camera::getOrthographicSize() const
	{
		return mOrthographicSize;
	}

	Float Camera::getOrthographicNear() const
	{
		return mOrthographicNear;
	}

	Float Camera::getOrthographicFar() const
	{
		return mOrthographicFar;
	}

	const Matrix4f& Camera::getOrthographicView() const
	{
		return mOrthographicView;
	}

	const Matrix4f& Camera::getOrthographicProjection() const
	{
		return mOrthographicProjection;
	}

	const Matrix4f& Camera::getOrthographicViewProjection() const
	{
		return mOrthographicViewProjection;
	}

	Float Camera::getPerspectiveFOV() const
	{
		return mPerspectiveFOV;
	}

	Float Camera::getPerspectiveNear() const
	{
		return mPerspectiveNear;
	}

	Float Camera::getPerspectiveFar() const
	{
		return mPerspectiveFar;
	}

	const Matrix4f& Camera::getPerspectiveView() const
	{
		return mPerspectiveView;
	}

	const Matrix4f& Camera::getPerspectiveProjection() const
	{
		return mPerspectiveProjection;
	}

	const Matrix4f& Camera::getPerspectiveViewProjection() const
	{
		return mPerspectiveViewProjection;
	}

	Vector3f Camera::getUpDirection() const
	{
		return glm::normalize(glm::rotate(mOrientation, CameraWorldUp));
	}

	Vector3f Camera::getRightDirection() const
	{
		return glm::normalize(glm::rotate(mOrientation, CameraWorldRight));
	}

	Vector3f Camera::getForwardDirection() const
	{
		return glm::normalize(glm::rotate(mOrientation, CameraWorldForward));
	}

	Vector3f Camera::getEulerAngles() const
	{
		return glm::eulerAngles(mOrientation) * 180.0f * OneOverPI;
	}

	Vector3f Camera::getEulerRadians() const
	{
		return glm::eulerAngles(mOrientation);
	}

	void Camera::setPosition(const Vector3f& position)
	{
		mPosition = position;
		recalculateOrthographicView();
		recalculatePerspectiveView();
	}

	void Camera::setOrthographicSize(const Float size)
	{
		mOrthographicSize = size;
		recalculateOrthographicProjection();
	}

	void Camera::setOrthographicNear(const Float nearPlane)
	{
		mOrthographicNear = nearPlane;
		recalculateOrthographicProjection();
	}

	void Camera::setOrthographicFar(const Float farPlane)
	{
		mOrthographicFar = farPlane;
		recalculateOrthographicProjection();
	}

	void Camera::setPerspectiveFOV(const Float fov)
	{
		mPerspectiveFOV = fov;
		recalculatePerspectiveProjection();
	}

	void Camera::setPerspectiveNear(const Float nearPlane)
	{
		mPerspectiveNear = nearPlane;
		recalculatePerspectiveProjection();
	}

	void Camera::setPerspectiveFar(const Float farPlane)
	{
		mPerspectiveFar = farPlane;
		recalculatePerspectiveProjection();
	}

	void Camera::setEulerAngles(const Vector3f& eulerAngles)
	{
		mOrientation = Quaternion(eulerAngles);
		recalculatePerspectiveView();
	}

	void Camera::setEulerRadians(const Vector3f& eulerRadians)
	{
		mOrientation = Quaternion(eulerRadians * 180.0f * OneOverPI);
		recalculatePerspectiveView();
	}

	void Camera::recalculateOrthographicView()
	{
		SE_PROFILE_FUNCTION();

		mOrthographicView = glm::inverse(glm::translate(Matrix4f(1.0f), mPosition));
		mOrthographicViewProjection = mOrthographicProjection * mOrthographicView;
	}

	void Camera::recalculateOrthographicProjection()
	{
		SE_PROFILE_FUNCTION();

		auto backgroundSize = RenderAPI::GetBackgroundSize();
		auto aspectRatio = Float(backgroundSize.x) / Float(backgroundSize.y);

		mOrthographicProjection = glm::ortho(
			-aspectRatio * mOrthographicSize * 0.5f,
			 aspectRatio * mOrthographicSize * 0.5f,
			-mOrthographicSize * 0.5f,
			 mOrthographicSize * 0.5f,
			 mOrthographicNear,
			 mOrthographicFar);
		mOrthographicViewProjection = mOrthographicProjection * mOrthographicView;
	}

	void Camera::recalculateOrthographicMatrices()
	{
		SE_PROFILE_FUNCTION();

		auto backgroundSize = RenderAPI::GetBackgroundSize();
		auto aspectRatio = Float(backgroundSize.x) / Float(backgroundSize.y);

		// TODO: Rotation.
		mOrthographicView = glm::inverse(glm::translate(Matrix4f(1.0f), mPosition));
		mOrthographicProjection = glm::ortho(
			-aspectRatio * mOrthographicSize * 0.5f,
			 aspectRatio * mOrthographicSize * 0.5f,
			-mOrthographicSize * 0.5f,
			 mOrthographicSize * 0.5f,
			 mOrthographicNear,
			 mOrthographicFar);
		mOrthographicViewProjection = mOrthographicProjection * mOrthographicView;
	}

	void Camera::recalculatePerspectiveView()
	{
		SE_PROFILE_FUNCTION();

		mPerspectiveView = glm::inverse(glm::translate(Matrix4f(1.0f), mPosition) * glm::toMat4(mOrientation));
		mPerspectiveViewProjection = mPerspectiveProjection * mPerspectiveView;
	}

	void Camera::recalculatePerspectiveProjection()
	{
		SE_PROFILE_FUNCTION();

		auto backgroundSize = RenderAPI::GetBackgroundSize();
		auto aspectRatio = Float(backgroundSize.x) / Float(backgroundSize.y);

		mPerspectiveProjection = glm::perspective(glm::radians(mPerspectiveFOV), aspectRatio, mPerspectiveNear, mPerspectiveFar);
		mPerspectiveViewProjection = mPerspectiveProjection * mPerspectiveView;
	}

	void Camera::recalculatePerspectiveMatrices()
	{
		SE_PROFILE_FUNCTION();

		auto backgroundSize = RenderAPI::GetBackgroundSize();
		auto aspectRatio = Float(backgroundSize.x) / Float(backgroundSize.y);

		mPerspectiveView = glm::inverse(glm::translate(Matrix4f(1.0f), mPosition) * glm::toMat4(mOrientation));
		mPerspectiveProjection = glm::perspective(glm::radians(mPerspectiveFOV), aspectRatio, mPerspectiveNear, mPerspectiveFar);
		mPerspectiveViewProjection = mPerspectiveProjection * mPerspectiveView;
	}
} // !namespace SE